package com.domino.shared.model;

import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.domino.shared.Player;

public class SimpleBoard implements Board, Cloneable {

	public static int HUGE_SIZE = 12;
	public static int LARGE_SIZE = 10;
	public static int NORMAL_SIZE = 8;
	public static int SMALL_SIZE = 6;
	public static int TINY_SIZE = 4;

	private Color[][] colors;
	private Set<Point> boundaryPoints;
	private Set<Point> blackAvailablePoints;
	private Set<Point> whiteAvailablePoints;
	private Map<Color, Integer> colorCount;
	List<Brick> bricks;
	private int size;

	public SimpleBoard() {
		this(NORMAL_SIZE);
	}

	public SimpleBoard(SimpleBoard other) {
		this.size = other.size;
		colors = new Color[size][size];
		for (int i = 0; i < other.size; i++) {
			for (int j = 0; j < other.size; j++) {
				colors[i][j] = other.colors[i][j];
			}
		}
		boundaryPoints = new LinkedHashSet<Point>(other.boundaryPoints);
		blackAvailablePoints = new LinkedHashSet<Point>(
				other.blackAvailablePoints);
		whiteAvailablePoints = new LinkedHashSet<Point>(
				other.whiteAvailablePoints);
		bricks = new LinkedList<Brick>(other.bricks);
		colorCount = new HashMap<Color, Integer>(other.colorCount);
	}

	public SimpleBoard(int size) {
		assertEvenSize(size);
		this.size = size;
		colors = new Color[size][size];
		boundaryPoints = new LinkedHashSet<Point>();
		blackAvailablePoints = new LinkedHashSet<Point>();
		whiteAvailablePoints = new LinkedHashSet<Point>();
		bricks = new LinkedList<Brick>();
		colorCount = new HashMap<Color, Integer>();
		colorCount.put(Color.BLACK, 0);
		colorCount.put(Color.WHITE, 0);
		int point = size / 2;
		putBrickInternal(new Brick(new Point(point - 1, point - 1), Color.BLACK));
		putBrickInternal(new Brick(new Point(point, point), Color.BLACK));
		putBrickInternal(new Brick(new Point(point - 1, point), Color.WHITE));
		putBrickInternal(new Brick(new Point(point, point - 1), Color.WHITE));
	}

	@Override
	public void putBrick(Brick brick) {
		if (brick.getPoint() == null) {
			bricks.add(brick);
			return;
		}
		assertAvailablePoint(brick);
		List<Point> flipped = putBrickInternal(brick);
		if (flipped.isEmpty()) {
			throw new IllegalStateException(
					"At least one should have flipped at brick " + brick);
		}
		bricks.add(brick);
	}

	@Override
	public void stopGame() {
		blackAvailablePoints = Collections.EMPTY_SET;
		whiteAvailablePoints = Collections.EMPTY_SET;
	}

	public Set<Point> getAvailblePoints(Color color) {
		assertNotNull(color, "color");
		return color.equals(Color.WHITE) ? whiteAvailablePoints
				: blackAvailablePoints;
	}

	public int getColorCount(Color color) {
		return colorCount.get(color);
	}

	@Override
	public Color getColor(Point point) {
		assertNotNull(point, "point");
		return colors[point.getX()][point.getY()];
	}

	private void assertNotNull(Object obj, String string) {
		if (obj == null) {
			throw new NullPointerException("Cannot be null " + string);
		}
	}

	@Override
	public String toString() {

		return toString(Color.WHITE);
	}

	public String toString(Color color) {
		String str = "";
		for (int y = 0; y < size; y++) {
			for (int x = 0; x < size; x++) {
				char p = '.';
				Point point = new Point(x, y);
				Color c = getColor(point);
				if (c != null) {
					p = c.equals(Color.WHITE) ? 'w' : 'b';
				}
				if (getAvailblePoints(color).contains(point)) {
					p = '*';
				}/*
				 * else if (boundaryPoints.contains(point)) { p='#'; }
				 */
				str += p;
			}
			str += "\n";
		}

		return "board=\n" + str + "]";
	}

	private List<Point> putBrickInternal(Brick brick) {
		Point point = brick.getPoint();
		Color color = brick.getColor();
		// System.out.println("Pre-Put brick "+point +" " + getColor(point));
		setPoint(point, color);
		// System.out.println("Put brick "+point +" " + getColor(point));
		// System.out.println(this.toString());
		// recurcive flip
		List<Point> flipped = new LinkedList<Point>();

		// new boundary
		for (int deltaX = -1; deltaX < 2; deltaX++) {
			for (int deltaY = -1; deltaY < 2; deltaY++) {
				Point pint = new Point(point.getX() + deltaX, point.getY()
						+ deltaY);
				if (isOutOfBound(pint)) {
					continue;
				}
				// System.out.println("* Direction dx="+deltaX +" dy=" + deltaY
				// + " -->"+pint + " " + getColor(pint));

				if (isEmpty(pint)) {
					boundaryPoints.add(pint);
				} else {
					boundaryPoints.remove(pint);

					if ((deltaX == 0 && deltaY == 0) == false) {
						List<Point> flippedPart = new LinkedList<Point>();
						flippedPart = flip(color, pint, deltaX, deltaY,
								flippedPart);
						flipped.addAll(flippedPart);
					}
				}
			}
		}

		// verify & update available lists
		blackAvailablePoints = updateAvailability(Color.BLACK, boundaryPoints);
		whiteAvailablePoints = updateAvailability(Color.WHITE, boundaryPoints);

		return flipped;
	}

	private void setPoint(Point point, Color color) {
		Color currentColor = getColor(point);
		if (currentColor == null) {
			// do nothing
		} else if (currentColor.equals(color) == false) {
			Color opposite = getOppositeColor(color);
			colorCount.put(opposite, colorCount.get(opposite) - 1);
		}
		colorCount.put(color, colorCount.get(color) + 1);
		colors[point.getX()][point.getY()] = color;
	}

	private Color getOppositeColor(Color color) {
		return color.equals(Color.WHITE) ? Color.BLACK : Color.WHITE;
	}

	private Set<Point> updateAvailability(Color color,
			Set<Point> availableSource) {
		Set<Point> availableDest = new LinkedHashSet<Point>();

		for (Point point : availableSource) {
			if (isAvailable(color, point)) {
				availableDest.add(point);
			}
		}
		return availableDest;
	}

	private List<Point> flip(Color color, Point point, int deltaX, int deltaY,
			List<Point> flippedPart) {
		Point pint = new Point(point.getX() + deltaX, point.getY() + deltaY);
		// System.out.println("Direction dx="+deltaX +" dy=" + deltaY);
		if (isOutOfBound(pint)) {
			flippedPart.clear();
			return flippedPart;
		}
		// System.out.println("Checking1 "+point +" " + getColor(point));
		// System.out.println("Checking2 "+pint +" " + getColor(pint));
		if (isEmpty(pint)) {
			flippedPart.clear();
			return flippedPart;
		}

		if (getColor(point).equals(color) == false) {
			flippedPart.add(point); // flip this one
		} else if (flippedPart.size() == 0) {
			return flippedPart; // if first brick is your own stop the recursion
		}

		if (getColor(pint).equals(color) == true) { // recursion is done, do the
													// flip of colors
			for (Point p : flippedPart) {
				setPoint(p, color);
			}
			return flippedPart; // done!
		}

		return flip(color, pint, deltaX, deltaY, flippedPart);
	}

	private boolean isEmpty(Point p) {
		return colors[p.getX()][p.getY()] == null;
	}

	public boolean testIsAvailable(Color c, Point p) {
		return isAvailable(c, p);
	}

	private boolean isAvailable(Color color, Point point) {
		for (int deltaX = -1; deltaX < 2; deltaX++) {
			for (int deltaY = -1; deltaY < 2; deltaY++) {
				if (deltaX == 0 && deltaY == 0) {
					continue;
				}
				Point pint = new Point(point.getX() + deltaX, point.getY()
						+ deltaY);
				Point nextPint = new Point(pint.getX() + deltaX, pint.getY()
						+ deltaY);
				if (isOpponentColorEndedWithMineInitial(color, pint)
						&& isOpponentColorEndedWithMine(color, pint, nextPint,
								deltaX, deltaY)) {
					return true;
				}
			}
		}
		return false;
	}

	public boolean testIsOpponentColorEndedWithMine(Color color, Point point,
			Point nextPoint, int deltaX, int deltaY) {
		return isOpponentColorEndedWithMineInitial(color, point)
				&& isOpponentColorEndedWithMine(color, point, nextPoint,
						deltaX, deltaY);
	}

	private boolean isOpponentColorEndedWithMineInitial(Color color, Point point) {
		if (isOutOfBound(point)) {
			return false;
		}
		if (isEmpty(point)) {
			return false;
		}
		return getColor(point).equals(color) == false;
	}

	private boolean isOpponentColorEndedWithMine(Color color, Point point,
			Point nextPoint, int deltaX, int deltaY) {
		if (isOutOfBound(point)) {
			return false;
		}
		if (isEmpty(point)) {
			return false;
		}
		if (isOutOfBound(nextPoint)) {
			return false;
		}
		if (isEmpty(nextPoint)) {
			return false;
		}

		if (getColor(point).equals(color) == false
				&& getColor(nextPoint).equals(color) == true) {
			return true;
		}

		Point nextPint = new Point(nextPoint.getX() + deltaX, nextPoint.getY()
				+ deltaY);
		return isOpponentColorEndedWithMine(color, nextPoint, nextPint, deltaX,
				deltaY);
	}

	private boolean isOutOfBound(Point p) {
		if (p.getX() < 0)
			return true;
		if (p.getX() >= size)
			return true;
		if (p.getY() < 0)
			return true;
		if (p.getY() >= size)
			return true;

		return false;
	}

	private void assertEvenSize(int size) {
		if ((size / 2) * 2 != size) {
			throw new IllegalArgumentException(
					"The size must be an even number, was: " + size);
		}
	}

	private void assertAvailablePoint(Brick brick) {
		Set<Point> availablePoints = getAvailblePoints(brick.getColor());
		if (availablePoints.contains(brick.getPoint()) == false) {
			throw new IllegalArgumentException(
					"Tried to put a brick on a non-available point: " + brick);
		}
	}

	public Set<Point> getBoundary() {
		return this.boundaryPoints;
	}

	public List<Brick> getBricks() {
		return this.bricks;
	}

	@Override
	public int getRoundNumber() {
		return bricks.size();
	}

	@Override
	public int getSize() {
		return size;
	}

	@Override
	public Color getNextColor() {
		if (bricks.isEmpty()) {
			return Color.BLACK;
		}
		Color lastColor = bricks.get(bricks.size() - 1).getColor();
		Color nextColor = Color.getOpposite(lastColor);
		if (getAvailblePoints(nextColor).size() > 0) {
			return nextColor;
		} else if (getAvailblePoints(lastColor).size() > 0) {
			return lastColor;
		} else {
			return null;
		}
	}

	@Override
	public int getScore(Color positiveColor) {
		/*
		 * Corners 8 Borders 4 Next to borders 1 Others 2
		 */
		int score = 0;
		// 1 Others
		for (int i = 2; i < size - 2; i++) {
			for (int j = 2; j < size - 2; j++) {
				Color c = this.colors[i][j];
				if (c == null) {
					continue;
				}
				score += c.equals(positiveColor) ? 2 : -2;
			}
		}
		
		// 2,3 Next to borders & borders
		for (int i = 2; i < size - 2; i++) {
			//next to borders
			Color nextToBorder = this.colors[1][i];
			Color border = this.colors[0][i];
			score += evaluateBorderAndNextToBorder(positiveColor, border, nextToBorder);
			nextToBorder = this.colors[size-2][i];
			border = this.colors[size-1][i];
			score += evaluateBorderAndNextToBorder(positiveColor, border, nextToBorder);
			//next to borders
			nextToBorder = this.colors[i][1];
			border = this.colors[i][0];
			score += evaluateBorderAndNextToBorder(positiveColor, border, nextToBorder);
			nextToBorder = this.colors[i][size-2];
			border = this.colors[i][size-1];
			score += evaluateBorderAndNextToBorder(positiveColor, border, nextToBorder);
		}
		

		// 4. corners
		
		/**
		   000
		   0xx
		   0xx
		 */
		Color corner = this.colors[size - 1][size-1];
		Color nextToCorner = this.colors[size - 2][size-2];
		Color border1 = this.colors[size - 1][size-2];
		Color border2 = this.colors[size - 2][size-1];
		score += evaluateCorner(positiveColor, corner, nextToCorner, border1, border2);
		
		/**
		   0xx
		   0xx
		   000
		 */
		corner = this.colors[size - 1][0];
		nextToCorner = this.colors[size - 2][1];
		border1 = this.colors[size - 1][1];
		border2 = this.colors[size - 2][0];
		score += evaluateCorner(positiveColor, corner, nextToCorner, border1, border2);
		
		/**
		 * 000
		   xx0
		   xx0
		 */
		corner = this.colors[0][size - 1];
		nextToCorner = this.colors[1][size - 2];
		border1 = this.colors[0][size - 2];
		border2 = this.colors[1][size - 1];
		score += evaluateCorner(positiveColor, corner, nextToCorner, border1, border2);
		
		/**
		   xx0
		   xx0
		   000
		 */
		corner = this.colors[0][0];
		nextToCorner = this.colors[1][1];
		border1 = this.colors[0][1];
		border2 = this.colors[1][0];
		score += evaluateCorner(positiveColor, corner, nextToCorner, border1, border2);
		
		return score;
	}

	private int evaluateCorner(Color positiveColor, Color corner,
			Color nextToCorner, Color border1, Color border2) {
		int score = 0;
		if (corner==null) {
			if (nextToCorner!=null) {
				score += 0; //nextToCorner.equals(positiveColor) ? -16 : 16;
			}
		} else {
			score += corner.equals(positiveColor) ? 16 : -16;
			if (nextToCorner!=null) {
				score += nextToCorner.equals(positiveColor) ? 2 : -2;
			}
		}
		if (border1!=null) {
			score += border1.equals(positiveColor) ? 4 : -4;
		}
		if (border2!=null) {
			score += border2.equals(positiveColor) ? 4 : -4;
		}
		return score;
	}

	private int evaluateBorderAndNextToBorder(Color positiveColor, Color border, Color nextToBorder) {
		if (border==null) {
			if (nextToBorder!=null) {
				return 0; //nextToBorder.equals(positiveColor) ? -4 : 4;
			} else {
				return 0;
			}
		} else {
			int borderScore = border.equals(positiveColor) ? 4 : -4;
			if (nextToBorder!=null) {
				return borderScore + (nextToBorder.equals(positiveColor) ? 2 : -2);
			} else {
				return borderScore;
			}
		}
	}
	
	public String encodeBricks() {
		StringBuilder sb = new StringBuilder();
		Iterator<Brick> iter = bricks.iterator();
		if (iter.hasNext()) {
			Brick brick = iter.next();
			sb.append(brick.toShortString());
			while(iter.hasNext()) {
				sb.append("," + iter.next().toShortString());
			}
		}
		return sb.toString();
	}

}